import 'dart:async';
import 'dart:convert';

import 'converter_inner_type.dart';

/// Exception for ObjectConverter
class ConverterException implements Exception {
  /// The Exception message.
  final dynamic message;

  /// The stack trace.
  final StackTrace trace;

  ConverterException(this.message, [StackTrace? trace]) : trace = trace ?? StackTrace.current;

  @override
  String toString() {
    Object? message = this.message;
    if (message == null) return "$runtimeType";
    return "$runtimeType: $message";
  }
}

/// Interface for parse between object and mapping data
abstract class ObjectMapper {
  /// Parse object from mapping data
  Future<void> fromMap(dynamic map);

  /// Parse object to mapping data
  Future<dynamic> toMap();
}

/// Builder for object
abstract class TypeBuilder<T> {
  /// Build an object
  T build(String type);

  /// Build an object list
  List<T> buildList(String type) => <T>[];

  /// Build an object set
  Set<T> buildSet(String type) => <T>{};

  /// init object from mapping data
  Future<T?> fromMap(T obj, dynamic map);

  /// Parse object to mapping data
  Future<dynamic> toMap(T obj);
}

/// Builder for object that implements the [ObjectMapper] interface
abstract class MapperTypeBuilder<T extends ObjectMapper> extends TypeBuilder<T> {
  @override
  fromMap(T obj, dynamic map) async {
    await obj.fromMap(map);
    return obj;
  }

  @override
  toMap(T obj) async => await obj.toMap();
}

/// Quick builder for object that not implements the [TypeBuilder] interface
class QuickTypeBuilder<T> extends TypeBuilder<T> {
  final T Function(String type) _builder;
  final Future<T?> Function(T obj, dynamic map) _fromMap;
  final Future<dynamic> Function(T obj) _toMap;

  QuickTypeBuilder({
    required T Function(String type) builder,
    required Future<dynamic> Function(T obj) toMap,
    required Future<T?> Function(T obj, dynamic map) fromMap,
  })  : _builder = builder,
        _toMap = toMap,
        _fromMap = fromMap;

  @override
  T build(String type) => _builder(type);

  @override
  fromMap(T obj, dynamic map) async => _fromMap(obj, map);

  @override
  toMap(T obj) async => _toMap(obj);
}

class QuickMapperTypeBuilder<T extends ObjectMapper> extends MapperTypeBuilder<T> {
  final T Function(String type) _builder;
  QuickMapperTypeBuilder({
    required T Function(String type) builder,
  }) : _builder = builder;

  @override
  T build(String type) => _builder(type);
}

class JsonProcessor {
  Future<dynamic> decode(String json) async => await ObjectConverter().executor(
        () => jsonDecode(json),
      );
  Future<String> encode(dynamic obj) async => await ObjectConverter().executor(
        () => jsonEncode(obj),
      );
}

/// Converter for data and object
class ObjectConverter {
  static final ObjectConverter _instance = ObjectConverter._internal();

  final _typeBuilders = <String, TypeBuilder>{};

  var jsonProcessor = JsonProcessor();

  var executor = <T>(FutureOr<T> Function() task) {
    return task();
  };

  factory ObjectConverter() => _instance;

  ObjectConverter._internal() {
    innerTypeBuilder().forEach(
      (k, v) => _typeBuilder(v, k.toString()),
    );
  }

  void _typeBuilder<T>(TypeBuilder<T> builder, String type) {
    var ts = type;
    var match = subTypeReg.firstMatch(type);
    if (match != null) {
      var type1 = match.group(1);
      if (type1 == 'List' || type1 == 'Set') ts = match.group(2)!;
    }
    assert(ts != 'dynamic', 'Type[$type] with dynamic is not allowed');
    _typeBuilders[type] = builder;
    print(
      "registered type builder[$ts@$type] with: ${builder.runtimeType}",
    );
  }

  void typeBuilder<T>(TypeBuilder<T> builder) {
    _typeBuilder(builder, T.toString());
  }

  void quickBuilder<T>({
    required T Function(String type) builder,
    required Future<dynamic> Function(T obj) toMap,
    required Future<T> Function(T obj, dynamic map) fromMap,
  }) =>
      typeBuilder(
        QuickTypeBuilder<T>(
          builder: builder,
          toMap: toMap,
          fromMap: fromMap,
        ),
      );

  void quickMapperBuilder<T extends ObjectMapper>({
    required T Function(String type) builder,
  }) =>
      typeBuilder(
        QuickMapperTypeBuilder<T>(builder: builder),
      );

  /// Parse the string [json] to object of Type[T]
  ///
  /// @throws [ConverterException]
  Future<T?> decode<T>(String? json) async {
    if (json == null || json.isEmpty) return null;
    return await mapWith(await jsonProcessor.decode(json));
  }

  /// Parse the string [json] to object of Type[T]
  ///
  /// @throws [ConverterException]
  Future<T?> mapWith<T>(dynamic map, {T? target, String? type}) async {
    if (map == null) return null;
    var t = type ?? T.toString();
    t = t.replaceAll(RegExp(r'\?'), '');
    assert(t != 'dynamic', 'Type[$t] with dynamic is not allowed');
    var match = subTypeReg.firstMatch(t);
    if (match != null) {
      if (match.group(1) == 'Map') {
        if (map is! T) {
          throw ConverterException(
            "Unable to cast object[${map.runtimeType}] to type $T",
          );
        }
        return map;
      }
      var mainType = match.group(1);
      if (mainType == 'List' || mainType == 'Set') {
        return await _mapListWith(map, match.group(2)!, mainType == "Set") as T;
      }
    }
    var builder = _typeBuilders[t];
    if (builder == null) {
      if (map is T) {
        return map;
      }
      throw ConverterException(
        "Unable to find builder for type ${T.toString()}",
      );
    }
    dynamic obj = target ?? builder.build(t);
    if (obj is! T) {
      throw ConverterException(
        "Unable to cast object[${obj.runtimeType}] to type $T",
      );
    }
    try {
      return await builder.fromMap(obj, map);
    } catch (e) {
      if (e is ConverterException) rethrow;
      throw ConverterException(
        "decode from \"$map\" to $type failed: $e",
        StackTrace.current,
      );
    }
  }

  Future<dynamic> _mapListWith<T>(dynamic map, String itemType, [bool isSet = false]) async {
    if (itemType == 'dynamic') {
      if (map is! List) {
        throw ConverterException(
          "Expect the input is a List, but ${map.runtimeType} got",
        );
      }
      if (!isSet) {
        return map.toList();
      }
      return map.toSet();
    }
    var builder = _typeBuilders[itemType];
    if (builder == null) {
      throw ConverterException(
        "Unable to find builder for type $itemType",
      );
    }
    dynamic obj = isSet ? builder.buildSet(itemType) : builder.buildList(itemType);
    if (obj is! List<T> && obj is! Set<T>) {
      throw ConverterException(
        "Unable to cast object[${obj.runtimeType}] "
        "to type ${isSet ? 'Set' : 'List'}<$itemType>",
      );
    }
    if (map is Iterable) {
      for (var item in map) {
        obj.add(await mapWith(item, type: itemType));
      }
    }
    return obj;
  }

  /// Encode object to json string
  /// @throws [ConverterException]
  Future<String?> encode<T>(T? obj) async {
    if (obj == null) return null;
    try {
      return await jsonProcessor.encode(await toMap(obj));
    } catch (e) {
      if (_typeBuilders[T.toString()] == null) {
        throw ConverterException(
          "Unable to find builder for type ${T.toString()}",
        );
      }
      throw ConverterException(
        "encode object to json failed: $e",
        StackTrace.current,
      );
    }
  }

  /// Encode object to mapping data
  /// @throws [ConverterException]
  Future<dynamic> toMap<T>(T? obj) async {
    if (obj == null) return null;
    if (obj is Map) {
      var mapped = <dynamic, dynamic>{};
      for (var item in obj.entries) {
        mapped[item.key] = await toMap(item.value);
      }
      return mapped;
    }
    if (obj is Iterable) {
      var mapped = [];
      for (var item in obj) {
        mapped.add(await toMap(item));
      }
      return mapped;
    }
    var builder = _typeBuilders[obj.runtimeType.toString()];
    dynamic mapped = await builder?.toMap(obj);
    if (builder == null) {
      try {
        mapped = (obj as dynamic).toMap();
        mapped = await toMap(mapped is Future ? await mapped : mapped);
      } on NoSuchMethodError {
        // ignore
      }
    } else if (mapped is Map || mapped is List) {
      mapped = await toMap(mapped);
    }
    return mapped ?? obj;
  }

  /// Convert object of Type[S] to another object of Type[T]
  ///
  /// @throws [ConverterException]
  Future<T> convert<S, T>(S src, {T? target}) async {
    // var builder = _typeBuilders[src.runtimeType.toString()];
    // if (builder == null) {
    //   throw ConverterException(
    //     "Unable to find builder for type ${S.toString()}",
    //   );
    // }
    return (await mapWith(await toMap(src), target: target))!;
  }
}
